package com.ia2011.project1.service;

import java.util.ArrayList;
import java.util.Random;

import com.ia2011.project1.debug.Debug;
import com.ia2011.project1.domain.Action;
import com.ia2011.project1.domain.Constants;
import com.ia2011.project1.domain.Spot;
import com.ia2011.project1.domain.TAction;
import com.ia2011.project1.domain.User;
import com.ia2011.project1.math.Utils;
import com.ia2011.project1.strategy.TStrategy;

public class CarpoolingService extends Debug {
	
	private ArrayList<User> users;
	
	public CarpoolingService(Integer N, Integer M, TStrategy strategy) {
		Random rnd = new Random();
		if (M == -1) {
			M = Math.abs(rnd.nextInt(N));
		}
		while (M >= N) {
			M = Math.abs(rnd.nextInt(N));
		}
		info("New CarpoolingService instance -> \nN: " + N + " \nM: " + M + " \nStrategy: " + strategy.toString());
		line();
		info("Generating users of the system (total: "+N+")");
		generateUsers(N);
		printDriversList();
		printNonDriversList();
		line();
		info("Choosing non-drivers among them (total: "+M+")");
		chooseNonDrivers(N,M);
		printDriversList();
		printNonDriversList();
		line();
		
		if (strategy == TStrategy.GOOD_ASSIGMENT) {
			goodAssigment();
		} else if (strategy == TStrategy.BAD_ASSIGMENT) {
			badAssigment();
		} else if (strategy == TStrategy.EMPTY_ASSIGMENT) {
			emptyAssigment();
		}
	}
	
	public CarpoolingService(CarpoolingService carpoolingService) {
		ArrayList<User> newUsers = new ArrayList<User>();
		for (User user : carpoolingService.getUsers()) {
			User newUser = new User(user);
			newUsers.add(newUser);
		}
		setUsers(newUsers);
	}

	/**
	 * Operator 1
	 * @param driver
	 * @return
	 */
	public boolean addPassengerToDriver(User driver) {
		boolean foundAndAssigned = false;
		for (int i=0; i<users.size() && !foundAndAssigned; i++) {
			User user = users.get(i);
			if (!user.isDriver() && !user.isPickedUp()) {
				foundAndAssigned = true;
				
				ArrayList<Spot> plan = driver.getTransport().getPlan();
//				info("plan BEFORE insert:");
//				for (Spot planSpot : plan) {
//					info("spot: "+planSpot);
//				}
				Spot spotA = new Spot(user.getHome().getCoords(), new Action(TAction.PICK_USER, user.getId()));
				Spot spotB = new Spot(user.getWork().getCoords(), new Action(TAction.DROP_USER, user.getId()));
				plan.add(plan.size()-1, spotA); // insert before the last spot
				plan.add(plan.size()-1, spotB); // insert before the last spot but after spotA
//				info("plan AFTER insert:");
//				for (Spot planSpot : plan) {
//					info("spot: "+planSpot);
//				}
//				line();
				
				Integer wholeDistance = 0;
				
				for (int j=0; j < plan.size() - 1; j++) {
					Integer distance = Utils.getDistance(plan.get(j).getCoord(), plan.get(j+1).getCoord());
					wholeDistance += distance;
				}
				Double timeSpent = Utils.getTime(wholeDistance);
//				info("new timeSpent for driver = "+timeSpent);
				if (timeSpent > Constants.TOTAL_MINUTES) { // ROLLBACK! Let's find another user...
//					info("ROLLBACK! Time exceed!");
//					line();
					foundAndAssigned = false;
					plan.remove(spotA);
					plan.remove(spotB);
				}
			}
		}
		return foundAndAssigned;
	}
	
	/**
	 * Remove passenger from Plan with bigger distance (???)
	 */
	public boolean removePassengerFromDriver(User driver) {
		
		if (driver.getTransport().getPlan().size() > 2) {
			ArrayList<Spot> plan = driver.getTransport().getPlan();
			Spot spot = plan.get(plan.size() - 2); // getting previous spot of the last one (the last one is the spot of a driver)
			Integer userIdToDelete = spot.getAction().getUserId();
			users.get(userIdToDelete).setPickedUp(false);
//			info("plan BEFORE delete:");
//			for (Spot planSpot : plan) {
//				info("spot: "+planSpot);
//			}
			ArrayList<Spot> toRemove = new ArrayList<Spot>();
			for (Spot planSpot : plan) {
				if (planSpot.getAction().getUserId() == userIdToDelete) {
					toRemove.add(planSpot);
				}
			}
			plan.removeAll(toRemove);
//			info("plan AFTER delete:");
//			for (Spot planSpot : plan) {
//				info("spot: "+planSpot);
//			}
			return true;
		}
		return false;
	}
	
	/**
	 * Assigments and other functions
	 */
	
	public Double getSumDistance() {
		Double sum = 0.0;
		for (User user : users) {
			if (user.isDriver()) {
				sum += user.getWholeDistance();
			}
		}
		return sum;
	}
	
	private void emptyAssigment() {
		info("Bad assigment");
		line();
		Integer amountOfDrivers = amountOfDrivers();
		Integer amountOfNonDrivers = amountOfNonDrivers();
		info("Drivers amount is " + amountOfDrivers);
		info("NonDriver amount is " + amountOfNonDrivers);
		
		for (User driver : getUsers()) {
			if (driver.isDriver()) {
				ArrayList<User> userToAdd = new ArrayList<User>();
				boolean result = makePlanForDriver(driver, userToAdd);
			}
		}
	}
	
	private void badAssigment() {
		info("Bad assigment");
		line();
		Integer amountOfDrivers = amountOfDrivers();
		Integer amountOfNonDrivers = amountOfNonDrivers();
		info("Drivers amount is " + amountOfDrivers);
		info("NonDriver amount is " + amountOfNonDrivers);
		
		for (User driver : getUsers()) {
			if (driver.isDriver()) {
				if (amountOfDrivers.intValue() >= amountOfNonDrivers.intValue()) { // attach non-drivers randomly (line by line)
//					info("amountOfDrivers >= amountOfNonDrivers");
					User nextNonDriver = findNextNonDriver();
					if (nextNonDriver == null) {
//						info("[!] There is no more not assigned nondriver users in system");
					} else {
						nextNonDriver.setPickedUp(true); // mark that user will be picked up
						ArrayList<User> userToAdd = new ArrayList<User>();
						userToAdd.add(nextNonDriver);
						boolean result = makePlanForDriver(driver, userToAdd);
//						info("We've found some plan for specific driver (id="+driver.getId()+")");
					}
				} else {
//					info("amountOfDrivers < amountOfNonDrivers");
					
					Double value = ((double)amountOfNonDrivers/(double)amountOfDrivers);
					Double amountOfUsers = Math.ceil(value);
					
//					info("Every Driver could have at least "+amountOfUsers+" users");
					ArrayList<User> usersToAdd = new ArrayList<User>();
					
					for (int i=0; i<amountOfUsers; i++) {
						User nextNonDriver = findNextNonDriver();
						if (nextNonDriver == null) {
//							info("[!] There is no more not assigned nondriver users in system");
						} else {
							nextNonDriver.setPickedUp(true);
							usersToAdd.add(nextNonDriver);
						}
					}
					
					boolean result = makePlanForDriver(driver, usersToAdd);
//					info("We've found some plan for specific driver (id="+driver.getId()+")");
				}
			}
			
			// spots info
//			for (Spot spot : driver.getTransport().getPlan()) {
//				info("Spot on ("+spot.getCoord()+") with actions:");
//				info("- spot action: "+spot.getAction().getType());
//				info("- spot userId: "+spot.getAction().getUserId());
//				info("> ----------------------");
//			}
		}
	}

	
	private User findNextNonDriver() {
		for (User user : getUsers()) {
			if (!user.isDriver() && !user.isPickedUp()) {
				return user;
			}
		}
		return null;
	}

	/**
	 * 
	 */
	private void goodAssigment() {
		info("Good assigment");
		line();
		Integer amountOfDrivers = amountOfDrivers();
		Integer amountOfNonDrivers = amountOfNonDrivers();
		info("Drivers amount is " + amountOfDrivers);
		info("NonDriver amount is " + amountOfNonDrivers);
		
		for (User driver : getUsers()) {

			if (driver.isDriver()) {
				ArrayList<Spot> plan = driver.getTransport().getPlan();

				if (amountOfDrivers.intValue() >= amountOfNonDrivers.intValue()) {
//					info("amountOfDrivers >= amountOfNonDrivers");
					Integer nearestUserId = findNearestUser(driver);
					if (nearestUserId == -1) {
//						info("[!] There is no more not assigned nondriver users in system");
					} else {
						getUsers().get(nearestUserId).setPickedUp(true); // mark that user will be picked up
						ArrayList<User> userToAdd = new ArrayList<User>();
						userToAdd.add(getUsers().get(nearestUserId));
						boolean result = makePlanForDriver(driver, userToAdd);
						
//						info("We've found some plan for specific driver (id="+driver.getId()+")");
					}
				} else { // amountOfDrivers < amountOfNonDrivers 
//					info("amountOfDrivers < amountOfNonDrivers");
					
					Double value = ((double)amountOfNonDrivers/(double)amountOfDrivers);
					Double amountOfUsers = Math.ceil(value);
					
//					info("Every Driver could have at least "+amountOfUsers+" users");
					ArrayList<User> usersToAdd = new ArrayList<User>();
					for (int i=0; i<amountOfUsers; i++) {
						Integer nearestUserId = findNearestUser2(driver);
						if (nearestUserId == -1) {
//							info("[!] There is no more not assigned nondriver users in system");
						} else {
//							info("["+i+"] Driver (id = "+driver.getId()+") will have Passenger (id = "+nearestUserId+")");
							User user = getUsers().get(nearestUserId);
							user.setPickedUp(true);
							usersToAdd.add(user);
						}
					}
					
					boolean result = makePlanForDriver(driver, usersToAdd);
//					info("We've found some plan for specific driver (id="+driver.getId()+")");
				}
				
				
//				// info block, could be commented 
//				Integer wholeDistance = 0;
//				for (int i=0; i < plan.size() - 1; i++) { // -2?
//					Integer distance = Utils.getDistance(plan.get(i).getCoord(), plan.get(i+1).getCoord());
//					wholeDistance += distance;
//				}
//				Double timeSpent = Utils.getTime(wholeDistance);
//				info("Driver spent "+timeSpent+" minutes");
//				
//				// spots info
//				for (Spot spot : plan) {
//					info("Spot on ("+spot.getCoord()+") with actions:");
//					info("- spot action: "+spot.getAction().getType());
//					info("- spot userId: "+spot.getAction().getUserId());
//					info("> ----------------------");
//				}
			}
		}
		
		
		
		printNonDriversList();
//		info("maximum time possible (mins):" + Utils.getTime(Utils.getDistance(new Point(0, 0), new Point(10000, 10000))));
//		return !abortProcess;
	}

	private boolean makePlanForDriver(User driver, ArrayList<User> usersToAdd) {
		ArrayList<Spot> driverPlan = driver.getTransport().getPlan();
		
		// add driver a spot to pick himself up from home
		Spot driverHomeSpot = new Spot();
		driverHomeSpot.setCoord(driver.getHome().getCoords());
		driverHomeSpot.setAction(new Action(TAction.PICK_USER, driver.getId()));
		driverPlan.add(driverHomeSpot);
		
		if (usersToAdd.size() != 0) {
			 // first step, pickup nearest user
			User user = usersToAdd.get(0);
			Spot spot = new Spot();
			spot.setCoord(user.getHome().getCoords());
			spot.setAction(new Action(TAction.PICK_USER, user.getId()));
			driverPlan.add(spot);
			driver.getTransport().addPassenger(user);
			usersToAdd.remove(user);
		
//			info("next step: finding nearest user to previous (1st spot)");
			// next steps
			while(usersToAdd.size() != 0) {
								
				if (driver.getTransport().isHaveFreeSeat()) { // if we have free seat let's pickup nearby user to us
					// search for nearby user
					User minUser = usersToAdd.get(0);
					Integer distanceFromLastSpotToUserHome = Utils.getDistance(driverPlan.get(driverPlan.size() - 1).getCoord(), minUser.getHome().getCoords());
//					info("by default we've took first user from collection with distance from last spot to his home = "+distanceFromLastSpotToUserHome);
					for (User userNew : usersToAdd) { 
						if (userNew != minUser) {
							Integer distanceFromLastSpotToUserNewHome = Utils.getDistance(driverPlan.get(driverPlan.size() - 1).getCoord(), userNew.getHome().getCoords());
//							info("other user have distance = "+distanceFromLastSpotToUserNewHome);
							if (distanceFromLastSpotToUserNewHome < distanceFromLastSpotToUserHome) {
//								info("we've found new minimum distance!");
								distanceFromLastSpotToUserHome = distanceFromLastSpotToUserNewHome;
								minUser = userNew;
							}
						}
					}
					
					// add new spot
					Spot newSpot = new Spot();
					newSpot.setCoord(minUser.getHome().getCoords());
					newSpot.setAction(new Action(TAction.PICK_USER, minUser.getId()));
					driverPlan.add(newSpot);
					driver.getTransport().addPassenger(minUser);
					usersToAdd.remove(minUser); // we don't need this user anymore
//					line();
				} else {
					// search for users among our passengers who works nearby to our current position
					ArrayList<User> passengers = driver.getTransport().getPassengers();
					Integer distanceFromLastSpotToUser1Work = Utils.getDistance(driverPlan.get(driverPlan.size() - 1).getCoord(), passengers.get(0).getWork().getCoords());
					Integer distanceFromLastSpotToUser2Work = Utils.getDistance(driverPlan.get(driverPlan.size() - 1).getCoord(), passengers.get(1).getWork().getCoords());
					
					if (distanceFromLastSpotToUser1Work < distanceFromLastSpotToUser2Work) {
						Spot newSpot = new Spot();
						newSpot.setCoord(passengers.get(0).getWork().getCoords());
						newSpot.setAction(new Action(TAction.DROP_USER, passengers.get(0).getId()));
						driverPlan.add(newSpot);
						driver.getTransport().getPassengers().remove(0); // +1 free seat
					} else {
						Spot newSpot = new Spot();
						newSpot.setCoord(passengers.get(1).getWork().getCoords());
						newSpot.setAction(new Action(TAction.DROP_USER, passengers.get(1).getId()));
						driverPlan.add(newSpot);
						driver.getTransport().getPassengers().remove(0); // +1 free seat
					}
				}
				
				
			}
			
			// we need to bring users who are still passengers to their work
			while(driver.getTransport().getPassengers().size() != 0) {
				if (driver.getTransport().getPassengers().size() > 1) { // there is only 2 passengers left
					Integer distanceFromLastSpotToUser1Work = Utils.getDistance(driverPlan.get(driverPlan.size() - 1).getCoord(), driver.getTransport().getPassengers().get(0).getWork().getCoords());
					Integer distanceFromLastSpotToUser2Work = Utils.getDistance(driverPlan.get(driverPlan.size() - 1).getCoord(), driver.getTransport().getPassengers().get(1).getWork().getCoords());
					
					if (distanceFromLastSpotToUser1Work < distanceFromLastSpotToUser2Work) {
						Spot newSpot = new Spot();
						newSpot.setCoord(driver.getTransport().getPassengers().get(0).getWork().getCoords());
						newSpot.setAction(new Action(TAction.DROP_USER, driver.getTransport().getPassengers().get(0).getId()));
						driverPlan.add(newSpot);
						driver.getTransport().getPassengers().remove(0); // +1 free seat
					} else {
						Spot newSpot = new Spot();
						newSpot.setCoord(driver.getTransport().getPassengers().get(1).getWork().getCoords());
						newSpot.setAction(new Action(TAction.DROP_USER, driver.getTransport().getPassengers().get(1).getId()));
						driverPlan.add(newSpot);
						driver.getTransport().getPassengers().remove(0); // +1 free seat
					}
				} else { // there is only one passenger left
					Spot newSpot = new Spot();
					newSpot.setCoord(driver.getTransport().getPassengers().get(0).getWork().getCoords());
					newSpot.setAction(new Action(TAction.DROP_USER, driver.getTransport().getPassengers().get(0).getId()));
					driverPlan.add(newSpot);
					driver.getTransport().getPassengers().remove(0); // +1 free seat
				}
			}
			
			
		}
		
		// at last add driver a spot to get to his work
		Spot driverWorkSpot = new Spot();
		driverWorkSpot.setCoord(driver.getWork().getCoords());
		driverWorkSpot.setAction(new Action(TAction.DROP_USER, driver.getId()));
		driverPlan.add(driverWorkSpot);
		
		return true;
	}

	/**
	 * For the case 1 (when amount of drivers = amount of non-drivers) we interested in the nearest user with the total path time < Constants.TOTAL_MINUTES
	 * @param driver
	 * @return
	 */
	private Integer findNearestUser(User driver) {
//		info("findNearestUser1: driver id = "+driver.getId());
		Integer minUserId = -1;
		boolean found = false;
		for (int i=0; i<getUsers().size() && !found; i++) {
			User user = getUsers().get(i);
			if (!user.isDriver() 
					&& !user.isPickedUp()
					&& !isTimeExceed1(driver, user)) {
				found = true;
				minUserId = user.getId();
			}
		}
		
		if (minUserId == -1) return minUserId; // no need to search minimum, there is no more non-drivers
		
		Integer minFromDriverHomeToUserHome = Utils.getDistance(driver.getHome().getCoords(), getUsers().get(minUserId).getHome().getCoords());
		Integer minFromDriverWorkToUserWork = Utils.getDistance(driver.getWork().getCoords(), getUsers().get(minUserId).getWork().getCoords());
		
		for (User user : getUsers()) {
			if (!user.isDriver() 
					&& user.getId() != minUserId 
					&& !user.isPickedUp()
					&& !isTimeExceed1(driver, user)) {
//				info("driver id = "+driver.getId() + " & user id = "+user.getId());
//				info("minFromDriverHomeToUserHome = "+minFromDriverHomeToUserHome);
//				info("minFromDriverWorkToUserWork = "+minFromDriverWorkToUserWork);
				Integer distanceFromDriverHomeToUserHome = Utils.getDistance(driver.getHome().getCoords(), user.getHome().getCoords());
				Integer distanceFromDriverWorkToUserWork = Utils.getDistance(driver.getWork().getCoords(), user.getWork().getCoords());
//				info("distanceFromDriverHomeToUserHome = "+distanceFromDriverHomeToUserHome);
//				info("distanceFromDriverWorkToUserWork = "+distanceFromDriverWorkToUserWork);
				if (distanceFromDriverHomeToUserHome <= minFromDriverHomeToUserHome
						&& distanceFromDriverWorkToUserWork <= minFromDriverWorkToUserWork) {
//					info("new minimum ^^^");
					minUserId = user.getId();
					minFromDriverHomeToUserHome = distanceFromDriverHomeToUserHome;
					minFromDriverWorkToUserWork = distanceFromDriverWorkToUserWork;
				}
			}
		}
		
//		info("the nearest to the driver (id="+driver.getId()+") is user (id="+minUserId+")");
		return minUserId;
	}
	
	/**
	 * 
	 * @param driver
	 * @param user
	 * @return
	 */
	private boolean isTimeExceed1(User driver, User user) {
//		info("isTimeExceed1: driver id = "+driver.getId() + " & user id = "+user.getId());
		Integer distanceFromDriverHomeToUserHome = Utils.getDistance(driver.getHome().getCoords(), user.getHome().getCoords());
		Integer distanceFromUserHomeToUserWork = Utils.getDistance(user.getHome().getCoords(), user.getWork().getCoords());
		Integer distanceFromUserWorkToDriverWork = Utils.getDistance(user.getWork().getCoords(), driver.getWork().getCoords());
//		info("distanceFromDriverHomeToUserHome = "+distanceFromDriverHomeToUserHome+" | distanceFromUserHomeToUserWork = "+distanceFromUserHomeToUserWork+" | distanceFromUserWorkToDriverWork = "+distanceFromUserWorkToDriverWork);
		 
		Double time1 = Utils.getTime(distanceFromDriverHomeToUserHome);
		Double time2 = Utils.getTime(distanceFromUserHomeToUserWork);
		Double time3 = Utils.getTime(distanceFromUserWorkToDriverWork);
//		info("time1 = "+time1+" | time2 = "+time2+" | time3 = "+time3);
		
		Double sum = time1 + time2 + time3;
//		info("sum = "+sum);
//		line();
		
		if (sum > Constants.TOTAL_MINUTES) return true;
		
		return false;
	}
	
	/**
	 * 
	 * @param driver
	 * @return
	 */
	private Integer findNearestUser2(User driver) {
//		info("findNearestUser2: driver id = "+driver.getId());
		Integer minUserId = -1;
		boolean found = false;
		for (int i=0; i<getUsers().size() && !found; i++) {
			User user = getUsers().get(i);
			if (!user.isDriver() 
					&& !user.isPickedUp()) {
				found = true;
				minUserId = user.getId();
			}
		}
		
		if (minUserId == -1) return minUserId; // no need to search minimum, there is no more non-drivers
		
		Integer minFromDriverHomeToUserHome = Utils.getDistance(driver.getHome().getCoords(), getUsers().get(minUserId).getHome().getCoords());
		Integer minFromDriverWorkToUserWork = Utils.getDistance(driver.getWork().getCoords(), getUsers().get(minUserId).getWork().getCoords());
		
		for (User user : getUsers()) {
			if (!user.isDriver() 
					&& user.getId() != minUserId 
					&& !user.isPickedUp()) {
//				info("driver id = "+driver.getId() + " & user id = "+user.getId());
//				info("minFromDriverHomeToUserHome = "+minFromDriverHomeToUserHome);
//				info("minFromDriverWorkToUserWork = "+minFromDriverWorkToUserWork);
				Integer distanceFromDriverHomeToUserHome = Utils.getDistance(driver.getHome().getCoords(), user.getHome().getCoords());
				Integer distanceFromDriverWorkToUserWork = Utils.getDistance(driver.getWork().getCoords(), user.getWork().getCoords());
//				info("distanceFromDriverHomeToUserHome = "+distanceFromDriverHomeToUserHome);
//				info("distanceFromDriverWorkToUserWork = "+distanceFromDriverWorkToUserWork);
				if (distanceFromDriverHomeToUserHome <= minFromDriverHomeToUserHome
						&& distanceFromDriverWorkToUserWork <= minFromDriverWorkToUserWork) {
//					info("new minimum ^^^");
					minUserId = user.getId();
					minFromDriverHomeToUserHome = distanceFromDriverHomeToUserHome;
					minFromDriverWorkToUserWork = distanceFromDriverWorkToUserWork;
				}
			}
		}
		
//		info("the nearest to the driver (id="+driver.getId()+") is user (id="+minUserId+")");
		return minUserId;
	}

	/**
	 * Chooses users that are non-drivers
	 * @param amountOfAllUsers
	 * @param amountOfNonDrivers
	 */
	private void chooseNonDrivers(Integer amountOfAllUsers, Integer amountOfNonDrivers) {
		Random rnd = new Random();
		Integer nonDriverId = Math.abs(rnd.nextInt(amountOfAllUsers));
		for (int i=0; i<amountOfNonDrivers; i++) {
			while(!getUsers().get(nonDriverId).isDriver()) { // avoid zeroes and same equal ID's
				nonDriverId = Math.abs(rnd.nextInt(amountOfAllUsers));
			}
			getUsers().get(nonDriverId).setDriver(false);
		}
	}

	/**
	 * id of users starts from 1
	 * @param amount
	 */
	private void generateUsers(Integer amount) {
		setUsers(new ArrayList<User>());
		for (int i=0; i<amount; i++) {
			User user = new User(i);
//			user.info();
			getUsers().add(user);
		}
	}
	
	public Integer amountOfDrivers() {
		Integer amount = 0;
		for (User user : getUsers()) {
			if (user.isDriver()) amount++;
		}
		return amount;
	}
	
	public Integer amountOfNonDrivers() {
		Integer amount = 0;
		for (User user : getUsers()) {
			if (!user.isDriver()) amount++;
		}
		return amount;
	}
	
	public void printDriversList() {
		String list = "";
		for (User user : getUsers()) {
			if (user.isDriver()) {
				list += user.getId()+";";
			}
		}
		if (list.equals("")) list = "none";
		info("Drivers List: "+list);
	}
	
	public void printNonDriversList() {
		String list = "";
		for (User user : getUsers()) {
			if (!user.isDriver()) {
				list += user.getId()+"(p:"+user.isPickedUp()+");";
			}
		}
		if (list.equals("")) list = "none";
		info("NonDrivers List: "+list);
	}
	
	@Override
	public void info() {

	}

	public void setUsers(ArrayList<User> users) {
		this.users = users;
	}

	public ArrayList<User> getUsers() {
		return users;
	}
}
